/*
 * Created on 11-Jan-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package connect.dht.router;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import torrentlib.Debug;
import torrentlib.SystemTime;

import connect.dht.DHTLog;
import connect.dht.router.DHTRouterContact;
import connect.dht.router.DHTRouterContactAttachment;

/**
 * @author parg
 *
 */

public class 
DHTRouterNodeImpl
{
	private DHTRouterImpl	router;
	private int				depth;
	private boolean			contains_router_node_id;
	
	private List<DHTRouterContactImpl>	buckets;
	private List<DHTRouterContactImpl>	replacements;
	
	private DHTRouterNodeImpl	left;
	private DHTRouterNodeImpl	right;
	
	private long	last_lookup_time;
	
	protected
	DHTRouterNodeImpl(
		DHTRouterImpl					_router,
		int								_depth,
		boolean							_contains_router_node_id,
		List<DHTRouterContactImpl>		_buckets )
	{
		router					= _router;
		depth					= _depth;
		contains_router_node_id	= _contains_router_node_id;
		buckets					= _buckets;
	}
	
	protected int
	getDepth()
	{
		return( depth );
	}
	
	protected boolean
	containsRouterNodeID()
	{
		return( contains_router_node_id );
	}
	
	protected DHTRouterNodeImpl
	getLeft()
	{
		return( left );
	}
	
	protected DHTRouterNodeImpl
	getRight()
	{
		return( right );
	}
	
	protected void
	split(
		DHTRouterNodeImpl	new_left,
		DHTRouterNodeImpl	new_right )
	{
		buckets	= null;
		
		if ( replacements != null ){
			
				// we can get this when coming out of sleep mode
			
			// Debug.out( "DHTRouterNode: inconsistenct - splitting a node with replacements" );
			
			for ( DHTRouterContactImpl rep: replacements ){
				
				router.notifyRemoved( rep );
			}
			
			replacements = null;
		}
		
		left	= new_left;
		right	= new_right;
	}
		
	protected List
	getBuckets()
	{
		return( buckets );
	}
	
	protected List<DHTRouterContactImpl>
	getReplacements()
	{
		return( replacements );
	}
	
	protected void
	addNode(
		DHTRouterContactImpl	node )
	{
		// MGP: notify that node added to bucket
		node.setBucketEntry();
		router.notifyAdded(node);
		
		buckets.add( node );
		
		requestNodeAdd( node, false );
	}
	
	protected DHTRouterContact
	addReplacement(
		DHTRouterContactImpl	replacement,
		int						max_rep_per_node )
	{
		if ( max_rep_per_node == 0 ){
			
			return( null );
		}
		
			// we ping the oldest bucket entry only if we "improve" matters in the replacement 
		
		boolean	try_ping	= false;
		
		if( replacements == null ){
			
			try_ping	= true;
			
			replacements = new ArrayList<DHTRouterContactImpl>();
			
		}else{
				
			if ( replacements.size() == max_rep_per_node ){
				
					// if this replacement is known to be alive, replace any existing
					// replacements that haven't been known to be alive
				
				if ( replacement.hasBeenAlive() ){
					
					for (int i=0;i<replacements.size();i++){
						
						DHTRouterContactImpl	r = (DHTRouterContactImpl)replacements.get(i);
				
						if ( !r.hasBeenAlive()){
							
							try_ping	= true;
							
							// MGP: kicking out a replacement that was never alive
							router.notifyRemoved(r);
							
							replacements.remove(i);
							
							break;
						}
					}
					
						// no unknown existing replacements but this is "newer" than the existing
						// ones so replace the oldest one
					
					if ( replacements.size() == max_rep_per_node ){
						
						DHTRouterContactImpl removed = (DHTRouterContactImpl) replacements.remove(0);
						
						// MGP: kicking out the oldest replacement
						router.notifyRemoved(removed);
					}
				}else{
						
						// replace old unknown ones with newer unknown ones
					
					for (int i=0;i<replacements.size();i++){
						
						DHTRouterContactImpl	r = (DHTRouterContactImpl)replacements.get(i);
				
						if ( !r.hasBeenAlive()){
							
							// MGP: kicking out an older replacement that was never alive
							router.notifyRemoved(r);
							
							replacements.remove(i);
							
							break;
						}
					}
				}
			}else{
				
				try_ping	= true;
			}
		}
		
		if ( replacements.size() == max_rep_per_node ){
			
				// no room, drop the contact
			
			return( null );
		}
		
		// MGP: notify observers contact added as a replacement
		replacement.setReplacement();
		router.notifyAdded(replacement);
		
		replacements.add( replacement );
			
		if ( try_ping ){
			
			for (int i=0;i<buckets.size();i++){
				
				DHTRouterContactImpl	c = (DHTRouterContactImpl)buckets.get(i);
				
					// don't ping ourselves or someone already being pinged
				
				if ( !( router.isID(c.getID()) || c.getPingOutstanding())){
			
					c.setPingOutstanding( true );
					
					router.requestPing( c );
					
					break;
				}
			}
		}
		
		return( replacement );
	}
	
	protected DHTRouterContactImpl
	updateExistingNode(
		byte[]						node_id,
		DHTRouterContactAttachment	attachment,
		boolean						known_to_be_alive )
	{
		for (int k=0;k<buckets.size();k++){
			
			DHTRouterContactImpl	contact = (DHTRouterContactImpl)buckets.get(k);
			
			if ( Arrays.equals(node_id, contact.getID())){

				if ( known_to_be_alive ){
					
					// MGP: will update observers of updated status in this method
					alive( contact );
				}
				
					// might be the same node but back after a restart. we need to
					// treat this differently as we need to kick off the "store"
					// events as required. 
		
				int	new_id	= attachment.getInstanceID();

					// if the new-id is zero this represents us hearing about a contact
					// indirectly (imported or returned as a query). In this case we 
					// don't use this information as an indication of the target's
					// instance identity because it isn't!
				
				if ( new_id != 0 ){
					
					int	old_id 	= contact.getAttachment().getInstanceID();
					
					if (  old_id != new_id ){
						
						DHTLog.log( "Instance ID changed for " + 
									DHTLog.getString( contact.getID())+ 
									": old = " + old_id + ", new = " + new_id );
											
						contact.setAttachment( attachment );
						
							// if the instance id was 0, this means that it was unknown
							// (e.g. contact imported). We still need to go ahead and treat 
							// as a new node 
						
						requestNodeAdd( contact, old_id != 0 );
					}
				}

				return( contact );
			}
		}
		
			// check replacements as well
			
		if ( replacements != null ){
			
			for (int k=0;k<replacements.size();k++){
				
				DHTRouterContactImpl	contact = (DHTRouterContactImpl)replacements.get(k);
				
				if ( Arrays.equals(node_id, contact.getID())){
	
					if ( known_to_be_alive ){
						
						// MGP: will update observers of updated status in this method
						alive( contact );
					}
	
					return( contact );
				}
			}
		}
		
		return( null );
	}
	
	protected void
	alive(
		DHTRouterContactImpl	contact )
	{
		// DHTLog.log( DHTLog.getString( contact.getID()) + ": alive" );
			
		contact.setPingOutstanding( false );
		
		// record whether was alive
		boolean was_alive = contact.isAlive();
		
		if ( buckets.remove( contact )){
			
			contact.setAlive();
			
			if (!was_alive) {
				// MGP: notify observers that now alive
				router.notifyNowAlive(contact);
			}
			
			// MGP: simply reinserting, so do not notify observers that added to bucket
			buckets.add( contact );
			
		}else if ( replacements.remove( contact )){
					
			long	last_time = contact.getFirstFailOrLastAliveTime();
			
			contact.setAlive();
			
			if (!was_alive) {
				// MGP: notify observers that now alive
				router.notifyNowAlive(contact);
			}
			
				// this is a good time to probe the contacts as we know a 
				// replacement is alive and therefore in a position to replace a
				// dead bucket entry. Only do this if we haven't heard from this contact
				// recently
			
			if ( contact.getLastAliveTime() - last_time > 30000 ){
			
				for (int i=0;i<buckets.size();i++){
					
					DHTRouterContactImpl	c = (DHTRouterContactImpl)buckets.get(i);
					
						// don't ping ourselves or someone already being pinged
					
					if ( !( router.isID(c.getID()) || c.getPingOutstanding())){
				
						c.setPingOutstanding( true );
						
						router.requestPing( c );
						
						break;
					}
				}
			}
			
			// MGP: simply reinserting, so do not notify observers that added to replacments
			replacements.add( contact );		
		}
	}
	
	protected void
	dead(
		DHTRouterContactImpl	contact,
		boolean					force )
	{
		// DHTLog.log( DHTLog.getString( contact.getID()) + ": dead" );
		
		contact.setPingOutstanding( false );
		
		// record whether was failing
		boolean was_failing = contact.isFailing();
		
		if ( contact.setFailed() || force ){
			
				// check the contact is still present
			
			if ( buckets.remove( contact )){
				
				if (!was_failing) {
					// MGP: first notify observers that now failing
					router.notifyNowFailing(contact);
				}
				
				// MGP: notify that removed from bucket
				router.notifyRemoved(contact);
								
				if ( replacements != null && replacements.size() > 0 ){
					
						// take most recent alive one and add to buckets
					
					boolean	replaced	= false;
					
					for (int i=replacements.size()-1;i>=0;i--){
						
						DHTRouterContactImpl	rep = (DHTRouterContactImpl)replacements.get(i);
						
						if ( rep.hasBeenAlive()){
							
							DHTLog.log( DHTLog.getString( contact.getID()) + ": using live replacement " + DHTLog.getString(rep.getID()));
							
							// MGP: notify that a replacement was promoted to the bucket
							rep.setBucketEntry();
							router.notifyLocationChanged(rep);

							replacements.remove( rep );
							
							buckets.add( rep );
							
							replaced	= true;
							
							requestNodeAdd( rep, false );
							
							break;
						}
					}
					
						// non alive - just take most recently added
					
					if ( !replaced ){
						
						DHTRouterContactImpl	rep = (DHTRouterContactImpl)replacements.remove( replacements.size() - 1 );
					
						DHTLog.log( DHTLog.getString( contact.getID()) + ": using unknown replacement " + DHTLog.getString(rep.getID()));

						// MGP: notify that a replacement was promoted to the bucket
						rep.setBucketEntry();
						router.notifyLocationChanged(rep);
						
						buckets.add( rep );
						
							// add-node logic will ping the node if its not known to
							// be alive
						
						requestNodeAdd( rep, false );
					}
				}
			}else{
				
				if (!was_failing) {
					// MGP: first notify observers that now failing
					router.notifyNowFailing(contact);
				}
				
				// MGP: notify that removed from replacement list
				router.notifyRemoved(contact);
				
				replacements.remove( contact );
			}
		}
	}
	
	protected void
	requestNodeAdd(
		DHTRouterContactImpl	contact,
		boolean					definite_change )
	{
		// DOS problem here - if a node deliberately flicked between
		// instance IDs we'll get into an update frenzy. 

		long	now = SystemTime.getCurrentTime();
		
		if ( now - contact.getLastAddedTime() > 10000 ){

			contact.setLastAddedTime( now );
			
			router.requestNodeAdd( contact );
			
		}else{
			
				// only produce a warning if this is a definite change from one id to
				// another (as opposed to a change from "unknown" to another)
			
			if ( definite_change ){
				
				router.log( "requestNodeAdd for " + contact.getString() + " denied as too soon after previous ");
			}
		}
	}
	
	protected long
	getTimeSinceLastLookup()
	{
		long	now = SystemTime.getCurrentTime();
		
		if ( now < last_lookup_time ){
			
				// clock changed, don't know so make as large as possible
			
			return( Long.MAX_VALUE );
		}
		
		return( now - last_lookup_time );
	}
	
	protected void
	setLastLookupTime()
	{
		last_lookup_time = SystemTime.getCurrentTime();
	}
	
	public void
	print(
		String	indent,
		String	prefix )
	{
		if ( left == null ){
			
			router.log( 
					indent + prefix + 
					": buckets = " + buckets.size() + contactsToString( buckets) + 
					", replacements = " + (replacements==null?"null":( replacements.size() + contactsToString( replacements ))) + 
					(contains_router_node_id?" *":" ") +
					(this==router.getSmallestSubtree()?"SST":"") +
					" tsll=" + getTimeSinceLastLookup());
			
		}else{
			
			router.log( indent + prefix + ":" + (contains_router_node_id?" *":" ") +
							(this==router.getSmallestSubtree()?"SST":""));
			
			left.print( indent + "  ", prefix + "1"  );
						
			right.print( indent + "  ", prefix + "0" );
		}
	}
	
	protected String
	contactsToString(
		List	contacts )
	{
		StringBuilder sb = new StringBuilder( contacts.size()*64 );
		sb.append( "{" );
		
		for (int i=0;i<contacts.size();i++){
			
			if ( i > 0 ){
				sb.append( ", " );
			}
			((DHTRouterContactImpl)contacts.get(i)).getString( sb );
		}
		
		sb.append( "}" );
		return( sb.toString());	
	}
}
